## # Translatable strings ✨
## > Provides DSL for autotranslatable strings
## 
## With this module you can easily write multilanguage programs!
## 
## ## Minimal Example 👨‍🔬
## 
## .. code-block:: nim
##    translable:
##      "Hello, world!":
##        # "Hello, world!" by default
##        "ru" -> "Привет, мир!"
##        "fr" -> "Bonjour, monde!"
##    serve("127.0.0.1", 5000):
##      get "/":
##        return translate("Hello, world!")
## 

import
  std/macros,
  std/strformat,
  ../core/[exceptions]


type
  LanguageSettings* = object
    lang*: string


template i18n*(body: untyped): untyped =
  ## Shorthand for `translatable macro<#translatable.m,untyped>`_
  translatable:
    body


macro translatable*(body: untyped): untyped =
  ## Make translations for strings
  ## 
  ## > Use standalone file with your translations for good practice.
  ## 
  ## # Example
  ## 
  ## .. code-block:: nim
  ##    translatable:
  ##      # If language is unknown than "default" key.
  ##      # if it does not exists than used "My own string"
  ##      "My own string":
  ##        "default" -> "this you can see by default"
  ##        "ru" -> "Моя собственная строка"
  ##        "fr" -> "..."
  ## 
  let
    translations = ident"translates"
  var translatesStatement = newStmtList()
  when defined(js):
    translatesStatement.add(newVarStmt(
      postfix(translations, "*"), # newNimNode(nnkPragmaExpr).add(ident"translates", newNimNode(nnkPragma).add(ident"global")),
      newCall(
      newNimNode(nnkBracketExpr).add(
          ident"newTable", ident"cstring",
          newNimNode(nnkBracketExpr).add(
            ident"TableRef", ident"cstring", ident"string"
          )
        )
      )
    ))
  else:
    translatesStatement.add(newVarStmt(translations, newCall(
      newNimNode(nnkBracketExpr).add(
        ident"newTable", ident"string", ident"StringTableRef"
      )
    )))
  for s in body:
    if s.kind == nnkCall and s[0].kind in [nnkStrLit, nnkTripleStrLit] and s[1].kind == nnkStmtList:
      let source = s[0]  # source string
      translatesStatement.add(
        when defined(js):
          newAssignment(
            newNimNode(nnkBracketExpr).add(translations, source),
            newCall(newNimNode(nnkBracketExpr).add(
              ident"newTable", ident"cstring", ident"string"
            ))
          )
        else:
          newAssignment(
            newNimNode(nnkBracketExpr).add(translations, source),
            newCall("newStringTable")
          ),
        newAssignment(
          newNimNode(nnkBracketExpr).add(
            newNimNode(nnkBracketExpr).add(translations, source),
            newLit"default"
          ),
          source
        )
      )
      for t in s[1]:
        if t.kind == nnkInfix and t[0] == ident"->" and t[1].kind in [nnkStrLit, nnkTripleStrLit] and t[2].kind in [nnkStrLit, nnkTripleStrLit]:
          translatesStatement.add(
            newAssignment(
              newNimNode(nnkBracketExpr).add(
                newNimNode(nnkBracketExpr).add(translations, source),
                t[1]
              ),
              t[2]
            )
          )
        else:
          throwDefect(
            HpxTranslatableDefect,
            "Invalid translatable syntax: ",
            lineInfoObj(t)
          )
    else:
      throwDefect(
        HpxTranslatableDefect,
        "Invalid translatable syntax: ",
        lineInfoObj(s)
      )
  when defined(js):
    translatesStatement.add(
      quote("@") do:
        proc translateImpl*(self: string, variables: varargs[string, `$`]): string =
          if not translates.hasKey(self):
            return self
          let lang =
            if languageSettings.val.lang == "auto":
              ($navigator.language)[0..1]
            else:
              ($languageSettings.val.lang)[0..1]
          if not translates[self].hasKey(lang):
            format(translates[self]["default"], variables)
          else:
            format(translates[self][lang], variables)
    )
  return translatesStatement


macro translate*(self: string, variables: varargs[string]): string =
  ## Translates `self` string to current client language (SPA) or accept-language header (SSG/SSR)
  ## 
  ## .. Note::
  ##    in JS backend this works like procedure calling.
  ## 
  ## ### Example
  ## ```nim
  ## translatable:
  ##   "hello_world":
  ##     "default" -> "Hello, $1th world!"
  ##     "ru" -> "Привет, $1ый мир!"
  ## 
  ## echo translate("hello_world", "10")  # Hello, 10th world!
  ## ```
  when defined(js):
    return newCall("translateImpl", self, variables)
  else:
    let
      language =
        newCall("[]", newCall("$",
          newNimNode(nnkIfExpr).add(
            newNimNode(nnkElifBranch).add(
              newCall("==", newDotExpr(ident"languageSettings", ident"lang"), newLit"auto"),
              ident"acceptLanguage"
            ), newNimNode(nnkElse).add(
              newDotExpr(ident"languageSettings", ident"lang")
            )
          )),
          newCall("..", newLit(0), newLit(1))
        )
      sourceRaw =
        when self is static[string]:
          newLit(self)
        else:
          self
      translations = ident"translates"
      source = newCall("format", newNimNode(nnkBracketExpr).add(
        newNimNode(nnkBracketExpr).add(
          translations, sourceRaw
        ),
        language
      ))
      sourceDefault = newCall("format", newNimNode(nnkBracketExpr).add(
        newNimNode(nnkBracketExpr).add(
          translations, sourceRaw
        ),
        newLit"default"
      ))
    
    for i in variables:
      source.add(i)
      sourceDefault.add(i)
    
    result = newNimNode(nnkIfStmt).add(
      newNimNode(nnkElifBranch).add(
        newCall("not",
          newCall(
            "hasKey",
            newNimNode(nnkBracketExpr).add(
              translations, sourceRaw
            ),
            language
          )
        ),
        sourceDefault
      ), newNimNode(nnkElse).add(
        source
      )
    )
    when not (self is static[string]):
      result.insert(0, newNimNode(nnkElifBranch).add(
        newCall("not",
          newCall(
            "hasKey",
            translations, sourceRaw
          )
        ),
        sourceRaw
      ))
